0%

SpringAI — Recursive Advisors(顾问递归)

概述

递归 Advisor(Recursive Advisors) 是一种特殊类型的 Advisor,可以多次循环遍历下游 Advisor 链。这种模式在需要重复调用大语言模型(LLM)直到满足特定条件时非常有用,例如:

  • 循环执行工具调用,直到无需再调用任何工具
  • 验证结构化输出,并在验证失败时重试
  • 通过修改请求来实现评估(Evaluation)逻辑
  • 通过修改请求来实现重试逻辑

核心机制

CallAdvisorChain.copy(CallAdvisor after) 方法是实现递归 Advisor 模式的关键工具。它创建一个新的 Advisor 链,该链仅包含原始链中指定 Advisor 之后的 Advisor,并允许递归 Advisor 根据需要调用此子链。

这种方法确保了:

  • 递归 Advisor 可以循环遍历链中剩余的 Advisor
  • 链中的其他 Advisor 可以观察和拦截每次迭代
  • Advisor 链保持正确的执行顺序和可观测性
  • 递归 Advisor 不会重新执行它之前的 Advisor

内置的递归 Advisor

Spring AI 提供了两个演示此模式的内置递归 Advisor:

1. ToolCallingAdvisor(工具调用 Advisor)

ToolCallingAdvisor 将工具调用循环作为 Advisor 链的一部分来实现,而不是依赖模型内部的工具执行。这使得链中的其他 Advisor 能够拦截和观察工具调用的过程。

核心特性

  • 循环遍历 Advisor 链,直到 ToolExecutionEligibilityChecker 报告没有更多工具调用需要执行
  • 支持 “return direct” 功能 —— 当工具执行设置了 returnDirect=true 时,它会中断工具调用循环,并直接将工具执行结果返回给客户端应用程序,而不是将其发送回 LLM
  • 使用 callAdvisorChain.copy(this) 创建用于递归调用的子链
  • 通过 conversationHistoryEnabled 支持可配置的对话历史管理
  • 支持可插拔的 ToolExecutionEligibilityChecker,以自定义循环迭代的条件

使用示例

1
2
3
4
// 示例:将 ToolCallingAdvisor 注册到 ChatClient
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(new ToolCallingAdvisor())
.build();

对话历史管理

ToolCallingAdvisor 包含一个 conversationHistoryEnabled 配置选项,用于控制工具调用迭代期间对话历史的管理方式。

默认行为(conversationHistoryEnabled = true

Advisor 在工具调用迭代期间会在内部维护完整的对话历史。这意味着工具调用循环中的每次后续 LLM 调用都会包含所有之前的消息(用户消息、助手回复、工具回复)。

默认情况下,记忆 Advisor(DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER = HIGHEST_PRECEDENCE + 200)被放置在工具调用循环的外部ToolCallingAdvisor(位于 HIGHEST_PRECEDENCE + 300)在迭代期间内部管理对话历史。记忆 Advisor 在循环前加载一次历史,并在循环结束后仅持久化最终的用户/助手交互。这是推荐的配置,因为大多数 ChatMemoryRepository 实现不支持工具调用消息类型。

将记忆 Advisor 放入循环内部

仅当需要将记忆 Advisor 放置在工具调用循环内部时(order 高于 ToolCallingAdvisor.DEFAULT_ORDER),才使用 .disableInternalConversationHistory() 方法。此时,记忆 Advisor 会在每次迭代时处理历史记录。请注意,只有 InMemoryChatMemoryRepository 支持持久化工具调用消息;其他存储库应使用上述默认的外部循环设置。

1
2
3
// 禁用内部对话历史,将记忆管理交给循环内的记忆 Advisor
ToolCallingAdvisor advisor = new ToolCallingAdvisor();
advisor.disableInternalConversationHistory();

手动控制工具调用循环

默认情况下,ToolCallingAdvisor 是自动注册的,并在内部管理整个工具调用循环 —— 调用者仅接收最终的 LLM 答案。

当你需要完全控制循环时(例如,向 UI 流式传输中间进度、添加自定义可观测性、或在迭代之间应用条件逻辑),你可以选择退出自动注册,并手动驱动循环。

按调用禁用自动注册:

使用 AdvisorParams.toolCallingAdvisorAutoRegister(false) 在每次调用时禁用自动注册:

1
2
3
4
// 手动驱动工具调用循环
chatClient.prompt("Your question here")
.advisorParams(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call();

在循环内部放置自定义 Advisor:

作为手动驱动循环的替代方案,你可以通过将自定义 Advisor 的 order 值设置为大于 ToolCallingAdvisor.DEFAULT_ORDER(例如 HIGHEST_PRECEDENCE + 400),将其放置在 ToolCallingAdvisor 循环内部。这样的 Advisor 会在工具调用循环的每次迭代中被调用,而不仅仅是在结束时调用一次,这意味着它可以访问所有中间消息:

  • 在流式模式下 —— 它在 ToolCallingAdvisor 从出站流中过滤掉工具调用请求块之前,接收每次迭代中模型的原始块流(包括工具调用请求块)。
  • 在调用路径中 —— 每次后续请求中传递的对话历史包含上一次迭代的 ToolResponseMessage,因此该 Advisor 可以观察到工具调用请求及其响应。

这种模式允许你将中间块转发到辅助通道(SSE、WebSocket、日志),而不会中断工具调用循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个观察 Advisor,在每次工具调用迭代时执行
public class ToolCallObservingAdvisor implements CallAdvisor {
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 400; // 确保在 ToolCallingAdvisor 之后执行
}

@Override
public ChatClientResponse adviseCall(CallAdvisorChain chain, ChatClientRequest request) {
// 在此处观察或记录中间状态
ChatClientResponse response = chain.nextCall(request);
// 处理响应
return response;
}
}

注册观察 Advisor 和自动注册的 ToolCallingAdvisor:

1
2
3
4
5
6
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
new ToolCallingAdvisor(), // order: HIGHEST_PRECEDENCE + 300
new ToolCallObservingAdvisor() // order: HIGHEST_PRECEDENCE + 400
)
.build();

由于 ToolCallObservingAdvisor 的 order 为 HIGHEST_PRECEDENCE + 400,它被插入到自动注册的 ToolCallingAdvisor(order 为 HIGHEST_PRECEDENCE + 300之后,因此它会参与每次工具调用迭代。ToolCallingAdvisor 仍然会从返回给外部链的内容中过滤掉工具调用块,因此主调用者仅接收最终答案 —— 而观察 Advisor 负责辅助通道的发射。

这种方法使工具调用循环完全由框架管理,同时仍能让你完全可见地观察每个中间步骤,并且它与对话历史管理中描述的 conversationHistoryEnabled 和记忆 Advisor 模式自然配对。

Return Direct 功能

“return direct” 功能允许工具绕过 LLM,将其结果直接返回给客户端应用程序。这在以下情况下非常有用:

  • 工具的输出就是最终答案,不需要 LLM 处理
  • 希望通过避免额外的 LLM 调用来减少延迟
  • 工具结果应按原样返回,无需解释

当工具执行设置了 returnDirect=true 时,ToolCallingAdvisor 将:

  1. 正常执行工具调用
  2. ToolExecutionResult 中检测到 returnDirect 标志
  3. 跳出工具调用循环
  4. 将工具执行结果作为 ChatResponse(以工具的输出作为生成内容)直接返回给客户端应用程序

2. StructuredOutputValidationAdvisor(结构化输出验证 Advisor)

StructuredOutputValidationAdvisor 根据 JSON Schema 验证结构化的 JSON 输出,并在验证失败时重试调用,最多重试指定次数。

核心特性

  • 从期望的输出类型派生 JSON Schema,或接受预先提供的 Schema 字符串
  • 根据 Schema 验证 LLM 的响应
  • 如果验证失败则重试调用,最多可配置重试次数(默认:3 次)
  • 在重试尝试时,将验证错误消息附加到 Prompt 中,以帮助 LLM 纠正其输出
  • 使用 callAdvisorChain.copy(this) 创建用于递归调用的子链
  • 可选地支持自定义 JsonMapper 进行 JSON 处理

该 Advisor 可以通过 outputType(自动派生 Schema)或 outputJsonSchema(预先提供的 Schema 字符串)进行配置;这两个选项是互斥的

使用示例

使用 outputType

1
2
3
4
// 根据期望的输出类型自动派生 JSON Schema
StructuredOutputValidationAdvisor advisor = new StructuredOutputValidationAdvisor();
advisor.setOutputType(MyOutputClass.class); // 自动从类定义中派生 Schema
advisor.setMaxRetries(3); // 设置最大重试次数

使用预先提供的 JSON Schema 字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 提供预先定义好的 JSON Schema 字符串
String jsonSchema = """
{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name", "age"]
}
""";

StructuredOutputValidationAdvisor advisor = new StructuredOutputValidationAdvisor();
advisor.setOutputJsonSchema(jsonSchema);
advisor.setMaxRetries(3);

entity() 调用上直接启用 Schema 验证:

作为配置 Advisor 的替代方案,你可以使用 EntityParamSpecentity() 调用上直接启用 Schema 验证,而无需手动配置 Advisor:

1
2
3
4
// 在 entity() 调用中直接启用验证
chatClient.prompt("Extract user information from this text: John is 25 years old")
.entity(new EntityParamSpec(MyOutputClass.class).withSchemaValidation(true))
.call();

总结

递归 Advisor 是 Spring AI 中一个强大的扩展机制,它允许 Advisor 在链中多次循环执行。通过 CallAdvisorChain.copy() 方法,递归 Advisor 可以创建子链并重复调用,直到满足特定条件。ToolCallingAdvisorStructuredOutputValidationAdvisor 是两个典型的内置实现,分别解决了工具调用循环和结构化输出验证重试的常见需求。